Skip to content

Add HUD animation Lua API - #17295

Open
ZenonSeth wants to merge 1 commit into
luanti-org:masterfrom
ZenonSeth:add_hud_animation_api
Open

Add HUD animation Lua API#17295
ZenonSeth wants to merge 1 commit into
luanti-org:masterfrom
ZenonSeth:add_hud_animation_api

Conversation

@ZenonSeth

Copy link
Copy Markdown
Contributor

Goal of the PR:

Add smooth client-side keyframe animations for HUD elements via a new player:hud_animate(id, def) Lua API, along with adding new opacity field for HUD elements that can also be animated.

How does the PR work?

  • Adds a new TOCLIENT_HUDANIMATE network packet (0x65) that sends animation definitions from server to client
  • The server-side Lua API parses per-property animation tables (keyframes, easing, loop count) and serializes them
  • The client receives the definition, stores it on the HudElement, and interpolates values each frame in RenderingCore::step()
  • Supports 9 animatable properties: pos_x/y, scale_x/y, offset_x/y, size_x/y, opacity and four easing funcs: linear, ease-in, ease-out, ease-in-out (all quadratic)
  • Also adds opacity as a HUD element field (0-1), supported for image, text, statbar, and waypoint elements
  • Old clients safely ignore the new packet, so no backwards compat issues

Does it resolve any reported issue?

No.

Does this relate to a goal in the roadmap?

  • Section 2.3 (UI Improvements) mentions a unified API for HUDs and GUIs. This PR extends the HUD API with animation capabilities

If not a bug fix, why is this PR needed? What usecases does it solve?

The only way to animate any hud element currently is continuous calls to pos/scale/etc, and it won't be smooth.
This PR enables smooth client-side animations defined once and interpolated per-frame. Use cases include:

  • Fading notifications in/out
  • Pulsing or bouncing indicators, eg. health or hunger meters
  • Sliding HUD elements on/off screen
  • Shaking effects that decay
  • Any combination of the above running simultaneously on the same element

LLM/AI disclosure:

  • Some code (around networking stuff mostly) was co-authored with Claude (Anthropic). All changes were hand-reviewed by me, and tested in-game.

To do

This PR is Ready for Review.

How to test

LUA mod code to test this
-- HUD animation test
local hud_anim_test_ids = {}

local function find_health_hud_id(player)
  local all = player:hud_get_all()
  for id, def in pairs(all) do
    if def.text == "heart.png" and def.type == "statbar" then
      return id
    end
  end
  return nil
end

core.register_chatcommand("hudanimstop", {
  description = "Stop HUD animation test",
  func = function(name, param)
    local player = core.get_player_by_name(name)
    if not player or not hud_anim_test_ids[name] then
      return false, "No animation running"
    end
    player:hud_animate(hud_anim_test_ids[name], {})
    player:hud_remove(hud_anim_test_ids[name])
    hud_anim_test_ids[name] = nil

    local hp_id = find_health_hud_id(player)
    if hp_id then
      player:hud_animate(hp_id, {})
    end

    return true, "All animations stopped"
  end,
})

core.register_chatcommand("hudanim1", {
  description = "Test HUD animation",
  func = function(name, param)
    local player = core.get_player_by_name(name)
    if not player then return false end

    -- remove previous test element
    if hud_anim_test_ids[name] then
      player:hud_remove(hud_anim_test_ids[name])
    end

    local id = player:hud_add({
      type = "image",
      position = {x = 0.5, y = 0.5},
      offset = {x = 0, y = 0},
      text = "heart.png",
      scale = {x = 2, y = 2},
      alignment = {x = 0, y = 0},
      opacity = 1.0,
    })
    hud_anim_test_ids[name] = id

    player:hud_animate(id, {
      pos_x = {
        keyframes = { {0.2, 1.0}, {0.8, 1.0}, {0.2, 1.0} },
        easing = "easeinout",
        loop = -1,
      },
      opacity = {
        keyframes = { {1.0, 0.5}, {0.2, 0.5}, {1.0, 0.5} },
        easing = "easeinout",
        loop = -1,
      },
    })

    return true, "HUD animation started"
  end,
})

-- hudanim2: fade in and out (opacity only, no movement)
core.register_chatcommand("hudanim2", {
  description = "Test HUD animation - fade pulse",
  func = function(name, param)
    local player = core.get_player_by_name(name)
    if not player then return false end

    if hud_anim_test_ids[name] then
      player:hud_remove(hud_anim_test_ids[name])
    end

    local id = player:hud_add({
      type = "image",
      position = {x = 0.5, y = 0.3},
      offset = {x = 0, y = 0},
      text = "heart.png",
      scale = {x = 3, y = 3},
      alignment = {x = 0, y = 0},
      opacity = 1.0,
    })
    hud_anim_test_ids[name] = id

    player:hud_animate(id, {
      opacity = {
        keyframes = { {1.0, 0.8}, {0.0, 0.8}, {1.0, 0.8} },
        easing = "easeinout",
        loop = -1,
      },
    })
    return true, "Fade pulse started"
  end,
})

-- hudanim3: shake that dies down (play once, no loop)
core.register_chatcommand("hudanim3", {
  description = "Test HUD animation - decaying shake",
  func = function(name, param)
    local player = core.get_player_by_name(name)
    if not player then return false end

    if hud_anim_test_ids[name] then
      player:hud_remove(hud_anim_test_ids[name])
    end

    local id = player:hud_add({
      type = "image",
      position = {x = 0.5, y = 0.5},
      offset = {x = 0, y = 0},
      text = "heart.png",
      scale = {x = 2, y = 2},
      alignment = {x = 0, y = 0},
    })
    hud_anim_test_ids[name] = id

    player:hud_animate(id, {
      offset_x = {
        keyframes = {
          {20, 0.05}, {-20, 0.05},
          {15, 0.05}, {-15, 0.05},
          {10, 0.05}, {-10, 0.05},
          {5, 0.05},  {-5, 0.05},
          {2, 0.05},  {-2, 0.05},
          {0},
        },
        easing = "linear",
        loop = 0,
      },
    })
    return true, "Decaying shake started"
  end,
})

-- hudanim4: circular-ish motion (pos_x and pos_y with offset phase)
core.register_chatcommand("hudanim4", {
  description = "Test HUD animation - orbit motion",
  func = function(name, param)
    local player = core.get_player_by_name(name)
    if not player then return false end

    if hud_anim_test_ids[name] then
      player:hud_remove(hud_anim_test_ids[name])
    end

    local id = player:hud_add({
      type = "image",
      position = {x = 0.5, y = 0.5},
      offset = {x = 0, y = 0},
      text = "heart.png",
      scale = {x = 2, y = 2},
      alignment = {x = 0, y = 0},
    })
    hud_anim_test_ids[name] = id

    -- approximate circle with 4 keyframes
    player:hud_animate(id, {
      pos_x = {
        keyframes = { {0.6, 0.5}, {0.5, 0.5}, {0.4, 0.5}, {0.5, 0.5}, {0.6, 0.5} },
        easing = "easeinout",
        loop = -1,
      },
      pos_y = {
        keyframes = { {0.5, 0.5}, {0.4, 0.5}, {0.5, 0.5}, {0.6, 0.5}, {0.5, 0.5} },
        easing = "easeinout",
        loop = -1,
      },
    })
    return true, "Orbit motion started"
  end,
})

-- hudanim5: loop 3 times then stop
core.register_chatcommand("hudanim5", {
  description = "Test HUD animation - 3x loop then stop",
  func = function(name, param)
    local player = core.get_player_by_name(name)
    if not player then return false end

    if hud_anim_test_ids[name] then
      player:hud_remove(hud_anim_test_ids[name])
    end

    local id = player:hud_add({
      type = "image",
      position = {x = 0.5, y = 0.5},
      offset = {x = 0, y = 0},
      text = "heart.png",
      scale = {x = 2, y = 2},
      alignment = {x = 0, y = 0},
      opacity = 1.0,
    })
    hud_anim_test_ids[name] = id

    player:hud_animate(id, {
      pos_x = {
        keyframes = { {0.3, 0.4}, {0.7, 0.4}, {0.3, 0.4} },
        easing = "easeout",
        loop = 3,
      },
      opacity = {
        keyframes = { {1.0, 0.4}, {0.3, 0.4}, {1.0, 0.4} },
        easing = "easein",
        loop = 3,
      },
    })
    return true, "3x loop animation started"
  end,
})

-- hudanim6: fade the actual health bar
core.register_chatcommand("hudanim6", {
  description = "Test HUD animation - health bar fade",
  func = function(name, param)
    local player = core.get_player_by_name(name)
    if not player then return false end

    local id = find_health_hud_id(player)
    if not id then return false, "Health bar not found" end

    player:hud_animate(id, {
      opacity = {
        keyframes = { {1.0, 1.0}, {0.1, 1.0}, {1.0, 1.0} },
        easing = "easeinout",
        loop = -1,
      },
    })
    return true, "Health bar fade started (id=" .. id .. ")"
  end,
})

-- hudanim7: bounce the actual health bar
core.register_chatcommand("hudanim7", {
  description = "Test HUD animation - health bar bounce",
  func = function(name, param)
    local player = core.get_player_by_name(name)
    if not player then return false end

    local id = find_health_hud_id(player)
    if not id then return false, "Health bar not found" end

    player:hud_animate(id, {
      offset_y = {
        keyframes = { {-91, 0.3}, {-101, 0.3}, {-91, 0.3} },
        easing = "easeout",
        loop = -1,
      },
      opacity = {
        keyframes = { {1.0, 0.3}, {0.4, 0.3}, {1.0, 0.3} },
        easing = "easeinout",
        loop = -1,
      },
    })
    return true, "Health bar bounce started (id=" .. id .. ")"
  end,
})

Video of above mod code running

Recording.2026-06-25.090330.mp4

@Zughy Zughy added @ Script API @ Client / Audiovisuals UI/UX Roadmap The change matches an item on the current roadmap labels Jun 25, 2026
@ZenonSeth
ZenonSeth force-pushed the add_hud_animation_api branch from 821c4fc to ca8780a Compare June 25, 2026 16:26
@ZenonSeth

Copy link
Copy Markdown
Contributor Author

Force-pushed because I was fixing just 3 tiny typos in the lua_api.md I had made

@appgurueu

Copy link
Copy Markdown
Contributor

To be honest, I am a bit torn.

On the one hand, this is definitely a cool feature! And you have done the work already.
On the other hand, I know the cleaner architectural solution is SSCSM, and in this case exposing HUD manipulation to SSCSM is very straightforward. You add hud.(add|remove|get) APIs once, and then immediately all kinds of animation become possible, together with things like immediate client HUD responses to client events (well, once SSCSM gets a client event listener...).

Something like the animation API in this PR can then be implemented in Lua, on the server, with no dependence on the client version - be it part of the engine or a mod.

Of course there's the argument of "SSCSM won't be accessible to modders soon", but then there's the counterargument of "we should prioritize SSCSM over implementing features that SSCSM will facilitate greatly".

@ZenonSeth

Copy link
Copy Markdown
Contributor Author

@appgurueu Thanks for taking a look - I want to say that I don't think this PR and SSCSM are in conflict at all.

Yes, SSCSM will eventually let you do raw client-side HUD pos manipulation, and that's great. But keyframe animation with built-in easing is still a useful feature - it means not every modder has to reimplement the interpolation math themselves. Even in a world where SSCSM exists and exposes hud_set, a dedicated animation api that handles timing, easing curves, and looping cleanly is still useful. Otherwise every mod that wants a smooth fade or a bounce effect has to run a per-step callback and compute lerp values manually.

The client-side animation system this PR adds is self-contained and just adds to available things that CSM mods in general could use. When SSCSM lands and gains HUD access, it can call into this exact same system. The keyframe engine is already there - SSCSM just becomes another way to drive it.

The point about "we should prioritize SSCSM over implementing features that SSCSM will facilitate greatly" - I understand what you mean, but I'm not asking anyone to spend their time writing this feature over SSCSM - as you said, this work is already done, and as I said above, i think it actually adds and can help enchance CSM in general, whether SS or local.

In short, this isn't an either/or, in my view it's a code that both gives current mods some more freedom, and future SSCSM mods another useful local API to access.

@ZenonSeth

ZenonSeth commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

On a different note: I am interested in helping out development, including SSCSM actually.

I've done c++ in my professional programming career, though it was over 9yrs ago - I'm just not up to date on all latest c++ features, C++11 was the last standard i worked with - but that just means I'm behind on syntax, i still have a good understanding of how c++ works.

So if SSCSM needs more help, or the new UI rework (as I already posted on matrix....before the spam that just happened) - I'm def interested in helping out.

Edit: Actually as a first step, I can work on exposing HUD elements + the client-side animation API to current CSM - if that aligns with roadmap plans (otherwise no point in me spending time on it :))

@cx384

cx384 commented Jun 26, 2026

Copy link
Copy Markdown
Member

Apart from whether it's better to implement this with SSCSM or not, also note that a formspec replacement is planned, which may also affect the HUD. #6527
So I'm skeptical about adding new complex features to the HUD, if they have not been discussed in an issue before.
Though, I like the feature itself, it seems to be quite useful for pretty effects.
(Maybe the "HUD animation definition" should be generalized, such that it can be reused for other things in the future, but that's a future discussion if we consider adding this feature.)

@ZenonSeth

Copy link
Copy Markdown
Contributor Author

Hey @cx384 I was actually already aware of the UI rework, rubenwardy mentioned it on Discord, and I mentioned that I am interested in helping out with that too. That said, if the rework is still early stages, and it's unclear whether it'll even touch HUD specifically, there's a point to be made about adding this.

Either way my offer to help stands, happy to drop this PR and see if I can do something similar and beyond for UI rework.

@Zughy

Zughy commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

We can safely assume that the GUI/HUD rework has not been moving forward for months, @v-rob doesn't seem active

@ZenonSeth

Copy link
Copy Markdown
Contributor Author

Well if you want someone else to come in to help with the GUI/HUD rework, like I said, I'm interested and happy to follow specs or discuss and chip in ideas.

@ZenonSeth
ZenonSeth force-pushed the add_hud_animation_api branch from ca8780a to 22a3fe3 Compare July 3, 2026 18:44
@ZenonSeth

Copy link
Copy Markdown
Contributor Author

Updated branch, rebased on latest master as there were conflicts.

If you would really rather wait for SSCSM rather than providing this functionality now, let me know and close the PR!

@ZenonSeth

Copy link
Copy Markdown
Contributor Author

I have to say, having spent a bit of time getting to understand SSCSM a bit better, I think in their current state they're at least a year from initial usable release to server mods - and i think that's only possible if development is focused on them. The reality is that people are busy, there's other issues and improvements to make and things take time to discuss and agree upon.

As for this not being discussed in an issue before - I wasn't sure what the best way to discuss this is, but given I had working code I thought this PR can serve as discussion. If an issue would reach more people, I can open one too, link this PR and use that for discussion.

My personal opinion is that with the state of SSCSM - and that I really haven't seen anything concrete about the UI rework - I think there are definite benefits to merging this PR, It's fairly stand alone and its actual implementation can easily be moved to built-in CSMs later, without changing the existing API.

@ZenonSeth ZenonSeth closed this Jul 18, 2026
@ZenonSeth ZenonSeth reopened this Jul 18, 2026
@ZenonSeth
ZenonSeth force-pushed the add_hud_animation_api branch from 22a3fe3 to ef7ab85 Compare July 18, 2026 18:50
@ZenonSeth ZenonSeth closed this Jul 18, 2026
@ZenonSeth ZenonSeth reopened this Jul 19, 2026
@ZenonSeth
ZenonSeth force-pushed the add_hud_animation_api branch from ef7ab85 to 22a3fe3 Compare July 19, 2026 08:27
@Sheriff-Unit-3

Sheriff-Unit-3 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

My two cents: SSCSM is still a ways off and the new UI doesn't seem to be happening anytime soon. As a modder these are features that can be useful now. For example I3 would likely use this for it's notification api. Awards could benefit from it. So for me I'd much rather see this get added now, then put off until 2028 or 2029 when SSCSM's finally come out of experimental and a new GUI api gets under way. (Because why work on a new GUI when it may as well be done with SSCSM.)

My point is this: The work is done, it'd be helpful and nice, so don't kick the can down the road!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

@ Client / Audiovisuals Roadmap The change matches an item on the current roadmap @ Script API UI/UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants